<?php
namespace Common\Model;

class AdModel extends Model {
    protected $tableName = 'ad';

    /**
     * buildSearchExpr 
     * 组装查询条件
     * 
     * @param mixed $d  查询数据，一般来源于$_GET或$_POST
     * @param mixed $field  可查询的字段对照表，其结构为array(array('表单项目名称' => '数据库字段名称'))
     * @access public
     * @return void 返回值为数组，参见方法内部注释
     */
    public function buildSearchExpr($d, $field = NULL) {
        $expr = array();
        $dao  = array();
        $sortExpr = $this->pk . ' DESC';

        /**
         * 下标为表单中的项目名称，值为数据库中表的字段名称 
         */
        if(empty($field)) {
            $field = [                
                'uid'       => 'uid',
                'user_name' => 'user_name',
                'plan_id'   => 'plan_id',
                'plan_name' => 'plan_name',
                'plan_type' => 'plan_type',
                'status'    => 'status',
                'id'        => 'id',
                'width'     => 'width',
                'height'    => 'height',
                'type'      => 'type',
            ];
        }

        foreach($field as $k => $v) {
            $val = isset($d[$k]) ? trim($d[$k]) : '';

            if(in_array($k, array('width', 'height'))) {
                $val = (int)$val;

                if($val < 1) {
                    $val = '';
                }
            }

            if(strlen($val) < 1) {
                continue;
            }

            switch($k) {
                case 'user_name' :
                    $expr['user_name'] = array('EXP', "LIKE '%{$val}%'");
                    break;

                case 'plan_name' :
                    $expr['plan_name'] = array('EXP', "LIKE '%{$val}%'");
                    break;

                case 'plan_type' : 
                    $tmpExpr = " plan_type='{$val}' ";
                    break;

                default:
                    $expr[$v] = $d[$k];
            }

            if(!empty($tmpExpr)) {
                $expr['_string'] = empty($expr['_string']) ? $tmpExpr : "{$expr['_string']} AND {$tmpExpr}"; 
            }

            $dao[$k] = $d[$k];
        }

        /**
         * 处理多个查询项目合并到一起，然后只能选一项查询的情况
         * 保存到$dao中，用于回填至查询表单
         */
        foreach($d as $k => $v) {
            if(substr($k, 0, 12) == 'search_item_') {
                $dao[$k] = $v;
            }
        }

        /**
         * 因为部分查询会涉及到视图，所以需要在下面指定完整的可排序字段
         */
        $sortField = array('uid', 'user_name', 'plan_id', 'plan_name', 'plan_type', 'type', 'status', 'ad_id', 'size', 'create_time', 'weight');

        if(!empty($d['sort']) && in_array($d['sort'], $sortField)) {
            $sortdir = empty($d['sortdir'])                     ? 'DESC'   : strtoupper($d['sortdir']);
            $sortdir = in_array($sortdir, array('DESC', 'ASC')) ? $sortdir : 'DESC';

            if($d['sort'] == 'size') {
                $sort     = 'size';
                $sortExpr = "width {$sortdir}, height {$sortdir}";
            } else {
                $sort     = $d['sort'];
                $sortExpr = "{$sort} {$sortdir}";
            }

            $dao['sort']    = $sort;
            $dao['sortdir'] = $sortdir;
        }

        return array(
            'expr' => $expr,            //ThinkPHP查询条件表达式
            'sort' => $sortExpr,        //排序表达式
            'json' => json_encode($dao) //用于回填至查询表单的数据
        );
    }

    public function save2($d) {
        $action = 'add';
        $id     = empty($d['id']) ? 0 : intvals($d['id']);

        if (empty($d[$this->pk])) {
            if (empty($d['plan_id'])) {
                throw new \Exception('没有计划编号，不能增加广告');
            }

            $planId = $d['plan_id'];
            $d['create_time'] = date('Y-m-d H:i:s');
        } else {
            $action = 'save';

            if (!$adRecord = $this->where(['id' => $id])->find()) {
                throw new \Exception('没有找到广告信息，无法保存');
            }

            $planId = $adRecord['plan_id'];
        }

        $planModel = D('Plan');

        if (!$planRecord = $planModel->where(['id' => $planId])->find()) {
            throw new \Exception('没有找到相关的计划信息，无法保存');
        }

        $planType = $planRecord['type'];

        if($action == 'add') {
            $d['uid'] = $planRecord['uid'];
        }

        $adType = empty($d['type']) ? null : strtolower($d['type']);

        if (empty($adType)) {
            throw new \Exception('请选择广告类型');
        }

        $relationAdTypes = getRelationAdTypes($planType);

        if (!in_array($d['type'], $relationAdTypes)) {
            throw new \Exception('选择定的广告类型有误');
        }

        try {
            if (!empty($d['code'])) {
                $d['code'] = $d['code'];
            } else {
                if (in_array($adType, ['tw', 'zn'])) {
                    $d['code'] = $d['html_code'];
                } elseif ($adType == 'zl') {
                    $d['code'] = $this->buildZlCode($d);
                } elseif ($adType == 'img') {
                    $d['code'] = $d['img_url'];
                }
            }
        } catch(\Exception $e) {
            throw new \Exception($e->getMessage());
        }

        $r = $this->$action($d);

        if (!$r) {
            return $r;
        }

        $id = $action == 'add' ? $r : $d['id'];

        try {
            cacheApi('ad', [
                'a'  => ($action == 'add' ? 'add' : 'modify'),
                'id' => $id,
            ]);
        } catch(Exception $e) {
            throw new \Exception($e->getMessage());
        }

        return $r;
    }

    /**
     * del
     *
     * @param mixed $id
     * @return void
     */
    public function del($id) {
        $id   = intvals($id);
        $expr = [
            'day'   => date('Y-m-d'),
            'ad_id' => $id,
        ];

        try {
            if (D('AdStats')->where($expr)->find()) {
                throw new \Exception('此广告今天已经有计费数据，无法删除');
            }
        } catch(\Exception $e) {
        }

        $expr = ['id' => $id];

        if (!$this->where($expr)->delete()) {
            throw new \Exception('删除数据出错');
        }

        try {
            cacheApi('ad', [
                'a'  => 'del',
                'id' => $id,
            ]);
        } catch(\Exception $e) {
            throw new \Exception($e->getMessage());
        }

        return true;
    }


    /**
     * del2
     * 可以删除单个，也可以删除多个
     *
     * @param mixed $id
     * @return void
     */
    public function del2($id) {
        $ids = (array)$id;

        try {
            foreach($ids as $id) {
                $this->del($id);
            }
        } catch(Exception $e) {
            throw new \Exception($e->getMessage());
        }

        return true;
    }

    /**
     * getAdsByTypeAndSize
     * 根据广告类型及尺寸返回广告记录
     *
     * @param mixed $type
     * @param mixed $width
     * @param mixed $height
     * @return void
     */
    public function getAdsByTypeAndSize($type, $width, $height) {
        $expr = array(
            'type'        => $type,
            'width'       => $width,
            'height'      => $height,
            'plan_status' => array('IN', array(1, 3, 4)),
        );

        $sort = 'plan_id DESC, ad_id DESC';
        $m    = M('vAd');
        $r    = array();

        if(!$rAd = $m->where($expr)->order($sort)->select()) {
            return false;
        }

        $znAdTypeConf = C('ZN_AD_TYPE_CONF');

        $znCodeStartTag    = '<!--tpl-->';
        $znCodeEndTag      = '<!--/tpl-->';
        $znCodeStartTagLen = strlen($znCodeStartTag);
        $znCodeEndTagLen   = strlen($znCodeEndTag);

        foreach($rAd as $v) {
            $planId = $v['plan_id'];

            if(empty($r[$planId])) {
                $r[$planId] = array(
                    'plan_id'   => $planId,
                    'plan_name' => $v['plan_name'],
                    'ads'       => array(),
                );
            }

            /**
             * 智能广告以<!--tpl--> <!--/tpl-->分隔出广告代码，其中两者之间的为主要广告代码
             */
            if(array_key_exists($v['type'], $znAdTypeConf)) {
                $first      = stripos($v['code'], $znCodeStartTag);
                $end        = strripos($v['code'], $znCodeEndTag);
                $len        = $end - $znCodeEndTagLen - $first;
                $v['code2'] = substr($v['code'], $first + $znCodeStartTagLen, $len);
            }

            $r[$planId]['ads'][] = $v;
        }

        return $r;
    }

    /**
     * getOutHtml
     * 返回适合输出的广告代码，此方法适用于预览广告代码
     *
     * @param mixed $type
     * @param mixed $url
     * @param mixed $width
     * @param mixed $height
     * @param mixed $code
     * @return void
     */
    public function getOutHtml($type, $url, $width, $height, $code = null) {
        $html = '';

        if(in_array($type, array('cpm', 'tt', 'xn'))) {
            $html = "弹窗地址：{$url}";
        } elseif(in_array($type, array('img', 'dl', 'pf'))) {
            $ext = strtolower(substr($code, -4));

            if($ext == '.swf') {
                $html = '<embed src="' . $code . '" allowscriptaccess="never" allownetworking="none" wmode="transparent" name="movie" '
                        . ' width="' . $width . '" height="' . $height . '"></embed>';
            } else {
                $html = "<img src='{$code}' width='{$width}' height='{$height}' />";
            }
        } else {
            $html = $code;
        }

        return $html;
    }

    /**
     * _options_filter()中设置的有条件，所以在Model::save()方法中不会再设置以主键为条件
     */
    protected function _before_update(&$data, &$options) {
        $pk = $this->getPk();

        if(!isset($options['where'][$pk]) && !isset($data[$pk])) {
            return false;
        }

        if(isset($options['where']) && !array_key_exists($this->getPk(), $options['where'])) {
            $options['where'][$this->getPk()] = $data[$this->getPk()];
        }

        return true;
    }

    protected function _options_filter(&$options) {
        if(!$loginUser = C('RUN_LOGIN_USER')) {
            exit('NO LOGIN');
        }

        /**
         * 为广告主平台数据绑定广告主编号
         */
        if($loginUser['type'] == 2) {
            $options['where']        = isset($options['where']) ? $options['where'] : array();
            $options['where']['uid'] = $loginUser['uid'];
        }
    }

    public function changeStatus($id, $status) {
        $id = intvals($id);

        if (empty($id)) {
            throw new \Exception('没有编号参数');
        }

        $status     = (int)$status;
        $statusConf = C('AD_STATUS');

        if(!array_key_exists($status, $statusConf)) {
            throw new \Exception('广告状态有误');
        }

        $expr = ['id'     => $id];
        $dao  = ['status' => $status];

        if (!$this->where($expr)->save($dao)) {
            return false;
        }

        try {
            cacheApi('ad', [
                'a'  => ($status == 1 ? 'modify' : 'del'),
                'id' => $id,
            ]);
        } catch(\Exception $e) {
            throw new \Exception($e->getMessage());
        }

        return true;
    }

    /**
     * startByPlanId
     * 开启计划关联的处于停止状态的广告
     *
     * @param mixed $planId
     * @return void
     */
    public function startByPlanId($planId) {
        $planId = intvals($planId);

        if (empty($planId)) {
            throw new \Exception('请指定计划编号');
        }

        $expr = ['plan_id' => $planId, 'status' => 3]; //只会开启处于停止状态的关联广告

        if(!$records = $this->where($expr)->select()) {
            return true;
        }

        foreach($records as $v) {
            $this->changeStatus($r['id'], 1);
        }

        return true;
    }

    /**
     * stopByPlanId
     * 关闭计划关联的处于投放状态的广告
     *
     * @param mixed $planId
     * @return void
     */
    public function stopByPlanId($planId) {
        $planId = intvals($planId);

        if (empty($planId)) {
            throw new \Exception('请指定计划编号');  
        }

        $expr = ['plan_id' => $planId];

        if (!$records = $this->where($expr)->select()) {
            return true;
        }

        foreach($records as $v) {
            $this->changeStatus($v['id'], 3);
        }

        return true;
    }

    /**
     * addFromZip
     * 以Zip压缩包的方式批量发布广告代码
     *
     * @param mixed $d = array(
     *  'plan_id' => '计划编号',
     *  'url_1' => 'PC端网址',
     *  'url_2' => 'Android端网址',
     *  'url_3' => 'IOS端网址',
     *  'url_4' => 'WP端网址',
     *  'add_base_href' => '是否改变或增加base href,值为真或假',
     *  'zip_file' => $_FILES['zip_file'],
     *  'status' => '广告默认状态’
     * )
     *
     * 可接收的网址参数以plan_device中投放终端设备为准
     * 如果相应终端没有设置网址则取用plan_device中对应网址
     * @return void
     */
    public function addFromZip($d) {
        /**
         * 检测参数是否完整
         */
        $planId = empty($d['plan_id']) ? 0 : (int)$d['plan_id'];

        if(empty($planId)) {
            throw new \Exception('没有广告计划编号');
        }

        if(!$rPlan = D2('Plan')->getByPlanid($planId)) {
            throw new \Exception('没有找到广告计划');
        }

        $planDeviceModel = D2('PlanDevice');

        if (!$planDeviceRecords = $planDeviceModel->where(array('plan_id' => $planId))->select()) {
            throw new \Exception('没有找到计划投放设备数据');
        }

        $urls = array();

        foreach ($planDeviceRecords as $v) {
            $_key = 'url_' . $v['pd_device'];
            $_url = empty($dao[$_key]) ? '' : getFullUrl(trim($dao[$_key]));
            $urls[$_key] = empty($_url) ? $v['pd_url'] : $_url;
        }

        $zipFileInfo = empty($d['zip_file']) || !is_array($d['zip_file']) ? null : $d['zip_file'];

        if(empty($zipFileInfo)) {
            throw new \Exception('没有找到广告代码压缩包');
        }

        $status = (int)$d['status'];

        /**
         * 复制zip文件到指定目录
         */
        $tmpFile = $zipFileInfo['tmp_name'];
        $toFile  = TEMP_PATH . md5(time() . $zipFileInfo['name']) . '.zip';

        if(!move_uploaded_file($tmpFile, $toFile)) {
            throw new \Exception('上传文件出错');
        }

        /**
         * 生成可被外网访问的目录保存与代码相关的辅助图片、样式表等
         */
        $zipTmpDir = TEMP_PATH . 'zipTmp';

        if(!is_dir($zipTmpDir)) {
            if(!mkdir($zipTmpDir)) {
                throw new \Exception('创建保存解压缩文件的临时目录出错');
            }
        }

        exec("/usr/bin/unzip {$toFile} -d {$zipTmpDir}");
        @unlink($toFile);

        $zipBaseDir        = $zipTmpDir . '/' . array_shift(explode('.', $zipFileInfo['name']));
        $adCodeResBasePath = C('AD_CODE_RES_FILE_BASE_PATH') . date('Y') . '/' . date('m') . '/' . time();
        $adCodeResFilePath = realpath(APP_PATH . '../'). $adCodeResBasePath;
        $baseHref          = getFullUrl(C('IMG_SERVER_URL') . $adCodeResBasePath . '/');

        if(!mkdir2($adCodeResFilePath)) {
            throw new \Exception('创建保存广告代码辅助目录出错');
        }

        try {
            copyDir($zipBaseDir, $adCodeResFilePath . '/');
        } catch(Exception $e) {
            throw new \Exception($e->getMessage());
        }

        /**
         * 扫描所有html或图片等进行保存
         */
        $typeConf        = array('dl', 'tw', 'img', 'pf', 'zn_tw', 'zn_text');   //可识别的广告类型
        $imgExtConf      = array('jpg', 'jpeg', 'png', 'gif', 'swf');    //默认识别为img的文件名扩展名
        $twExtConf       = array('html', 'htm'); //默认识别为tw的文件扩展名
        $defaultTypeConf = array('img' => $imgExtConf, 'tw' => $twExtConf); //自动识别广告类型时对应的文件名扩展名数据

        $cacheError = array();
        $error      = array();
            
        foreach(glob($zipBaseDir . '/*.*') as $v) {
            $fileName = basename($v);
            list($first, $ext) = explode('.', $fileName);
            $ext   = strtolower($ext);

            /**
             * 用横线分隔后，正常情况下有三部分，第一部分是广告尺寸，第二部分是广告类型，第三部分是附加字符或广告编号
             * 附加字符不能是纯数字，可以是字母数字混合。如果是纯数字就没法与广告编号区分开
             */
            $tmp   = explode('-', strtolower($first));
            $size  = explode('x', $tmp[0]);
            $type  = in_array($tmp[1], $typeConf) ? $tmp[1] : null;
            $_adId = empty($tmp[2]) ? null : (is_numeric($tmp[2]) ? $tmp[2] : null);
            $code  = null;

            if (!empty($_adId)) {
                if (!$_adRow = D2('Ad')->getByAdId($_adId)) {
                    $erro[] = "广告编号：{$_adId}，未找到此广告数据";

                    continue;
                } 

                if ($size[0] != $_adRow['width'] || $size[1] != $_adRow['height']) {
                    $erro[] = "广告编号：{$_adId}，与提供的新代码尺寸不匹配";

                    continue;
                }

                $type = $_adRow['type'];
            }

            if(count($size) < 2) {
                $error[] = $fileName . '尺寸不完整';

                continue;
            }

            /**
             * 未指定$type，启用自动识别广告类型机制
             */
            if(empty($type)) {
                foreach($defaultTypeConf as $k2 => $v2) {
                    if(in_array($ext, $v2)) {
                        $type = $k2;

                        break;
                    }
                }
            }

            if(empty($type)) {
                $error[] = $fileName . '无法识别广告类型';

                continue;
            }

            switch($type) {
                case 'img' :
                case 'dl' :
                case 'pf' :
                    $code = $baseHref . '/' . basename($v);

                    break;

                case 'zn_tw' :
                case 'zn_text' :
                case 'tw' :
                    $code = file_get_contents($v);
                    $code = preg_replace('/href=["|\']#["\']/', 'href="javascript:;"', $code);
                    $code = preg_replace('/<base\s+href=.+?\s*\/>/', '', $code);  //先删除原有的base href
                    $code = $this->replacePath($code, $baseHref);

                    break;

                default:
                    continue 2;
            }

            $dao = array(
                'plan_id' => $planId,
                'url'     => $url,
                'width'   => $size[0],
                'height'  => $size[1],
                'status'  => $status,
                'type'    => $type,
                'code'    => $code,
            );

            if (!empty($_adId)) {
                $dao['ad_id'] = $_adId;
            }

            $dao = array_merge($dao, $urls);

            try {
                $this->save2($dao);
            } catch(\Exception $e) {
                if(count($cacheError) > 10) {   //只收集10条cacheApi错误
                    continue;
                }

                $cacheError[] = $e->getMessage();
            } catch(Exception $e) {
                throw new \Exception($e->getMessage());
            }
        }

        removeDir($zipBaseDir); //删除解压缩后的文件

        $msg = '';

        if(!empty($error)) {
            $msg  = "部分广告发布出错，原因如下：";
            $msg .= implode("|", $error);
        }

        if(!empty($cacheError)) {
            $msg .= "生成缓存出错，原因如下：";
            $msg .= implode("|", $cacheError);
        }

        if(!empty($msg)) {
            throw new \Exception($msg);
        }

        return true;
    }

    /**
     * replacePath
     * 替换代码中的css和图片地址
     *
     * 只替换不是以http://开头的css文件和图片地址
     *
     * @param mixed $code
     * @param mixed $baseUrl
     * @return void
     */
    public function replacePath($code, $baseUrl) {
        $regx = array(
            '/<link(.*?)href=[\'|"](?!http:\/\/)(.*?)[\'|\"]/i',    //匹配被嵌入css的地址
            '/<img(.*?)src=[\'|"](?!http:\/\/)(.*?)[\'|"]/i',   //匹配img图片地址
        );

        foreach($regx as $v) {
            if(!preg_match_all($v, $code, $r)) {
                continue;
            }

            if(empty($r[2])) {
                continue;
            }

            $a = $r[2];
            $b = array();

            foreach($a as $v) {
                $b[] = $baseUrl . $v;
            }

            $code   = str_replace($a, $b, $code);
        }

        return $code;
    }

    /**
     * copy2
     * 复制广告
     *
     * @param mixed $d
     * @return void
     */
    public function copy2($d) {
        $adIds = empty($d['ad_id']) || !is_array($d['ad_id']) ? null : $d['ad_id'];

        if(empty($adIds)) {
            throw new \Exception('请指定需要复制的广告编号');
        }

        $destPlanId = empty($d['plan_b']) ? null : (int)$d['plan_b'];

        if(empty($destPlanId)) {
            throw new \Exception('请指定目标广告计划');
        }

        if(!$destPlanRow = D2('Plan')->getByPlanid($destPlanId)) {
            throw new \Exception('没有找到目标广告计划');
        }

        $planDeviceModel = D2('PlanDevice');
        $planDeviceExpr  = array('plan_id' => $destPlanId);

        /**
         * 验证目标广告网址
         * 如果关联设备没有指定网址则直接使用计划的网址
         */
        if (!$planDeviceRecords = $planDeviceModel->where($planDeviceExpr)->select()) {
            throw new \Exception("没有找到目标广告计划网址配置数据");
        }

        //计划投放设备对应的网址
        $urls = array(
            'url_1' => '',
            'url_2' => '',
            'url_3' => '',
            'url_4' => '',
        );    

        foreach ($planDeviceRecords as $v) {
            $_key = 'url_' . $v['pd_device'];
            $_url = empty($d[$_key]) ? '' : getFullUrl(trim($d[$_key]));
            $urls[$_key] = empty($_url) ? $v['pd_url'] : $_url;
        }

        $cacheError = array();  //cacheApi返回的错误信息

        foreach($adIds as $_adId) {
            if(!$_row = $this->getByAdId($_adId)) {
                continue;
            }

            $_dao = array(
                'plan_id' => $destPlanId,
                'uid'     => $destPlanRow['uid'],
                'type'    => $_row['type'],
                'width'   => $_row['width'],
                'height'  => $_row['height'],
                'code'    => $_row['code'],
                'status'  => 1,
            );

            $_dao = array_merge($_dao, $urls);

            try {
                $this->save2($_dao);
            } catch(\Exception $e) {
                if(count($cacheError) > 10) {   //只收集10条cacheApi错误
                    continue;
                }

                $cacheError[] = $e->getMessage();
            } catch(Exception $e) {
                throw new \Exception($e->getMessage());
            }
        }

        if(!empty($cacheError)) {
            throw new \Exception(implode("\n", $cacheError));
        }

        return true;
    }

    /**
     * buildZlCode
     * 组装直链保存时的代码
     * 将[zl_title => '', zl_img_url => '']转成json
     *
     * @param mixed $d
     * @return void
     */
    public function buildZlCode($d) {
        $title  = empty($d['zl_title'])   ? '' : trim($d['zl_title']);
        $imgUrl = empty($d['zl_img_url']) ? '' : trim($d['zl_img_url']);
        $e      = [];

        if (empty($title)) {
            $e[] = '请指定直链标题';
        }

        if (empty($imgUrl)) {
            $e[] = '请指定直链图片';
        }

        if ($e) {
            throw new \Exception(implode("\n", $e));
        }

        return json_encode(['zl_title' => $title, 'zl_img_url' => $imgUrl]);
    }

    /**
     * parseZlCode
     * 将直链code转换成数组
     *
     * @param mixed $code
     * @return void
     */
    public function parseZlCode($code) {
        if (empty($code)) {
            return [];
        }

        return json_decode($code, true);
    }

    public function isActive($s) {
        $s = (int)$s;

        return $s == 1 ? true : false;
    }

}
